<?php
/**
 * Library Of Shared Code (LOSC)
 *
 * LICENSE
 *
 * This source file is subject to the New BSD License that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @category   LOSC Framework
 * @package    Form
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */

/**
 * Class for generating a form from a Doctrine table or record
 *
 * @category   LOSC Framework
 * @package    Form
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */
class Losc_Form_Doctrine extends Zend_Form
{
    /**
     * Filter to use for generating element labels
     *
     * @var Zend_Filter_Word_UnderScoreToSeparator
     */
    protected static $_labelFilter;
    
    /**
     * All of doctrines supported column types
     * 
     * @var array
     */
    protected $_fieldTypes = array('boolean'    =>  'Checkbox',
                                   'integer'    =>  'Text',
                                   'float'      =>  'Text',
                                   'decimal'    =>  'Text',
                                   'string'     =>  array('Text', 'Textarea'),
                                   'time'       =>  'Text',
                                   'date'       =>  'Text',
                                   'enum'       =>  'Select',
                                   'array'      =>  null,
                                   'object'     =>  null,
                                   'blob'       =>  null,
                                   'clob'       =>  null,
                                   'timestamp'  =>  'Text',
                                   'gzip'       =>  null);
    
    /**
     * Columns that should be ignored
     *
     * @var array
     */
    protected $_ignoreColumns = array('___save', '_method');
    
    /**
     * Relations that should be ignored
     *
     * @var array
     */
    protected $_ignoreRelations = array();
    
    /**
     * Contains form elements for columns
     *
     * @var array
     */
    protected $_columnElements = array();
    
    /**
     * Whether relations should be included when generating form
     *
     * @var boolean
     */
    protected $_withRelations = true;
    
    /**
     * Generates form from the givne $table, optionally with relations
     *
     * @param  string|Doctrine_Table $table          table to generate from
     * @param  bool                  $withRelations  [optional] default is true
     * @return Losc_Form_Doctrine
     */
    public function generateFromTable($table, $withRelations = null)
    {
        if ($withRelations !== null) {
            $this->_withRelations = (bool) $withRelations;
        }
        
        if (!$table instanceof Doctrine_Table) {
            $table = Doctrine::getTable($table);
        }
        
        if (!$title = $table->getOption('comment')) {
            $title = $table->getOption('name');;
        }
        
        if ($t = $this->getTranslator()) {
            $title = $t->translate($title);
        }
        
        $this->setDescription($title);
        
        $tableCols        = $table->getColumns();
        if ($this->_withRelations && $relations = $table->getRelations()) {
            foreach ($relations as $alias => $relation) {
                //if (in_array($alias, $this->_ignoreRelations)) {
                //    continue;
                //}
                
                if (array_key_exists($alias, $tableCols)) {
                    // only ignore columns with a matching relation
                    $this->_ignoreColumns[] = $relation->getLocal();
                }
                
                $definition = $relation->toArray();
                if (!empty($definition['refTable'])) {
                    $this->_ignoreRelations[] =
                        $definition['refTable']->getOption('name');
                }
            }
        }
        //exit(__FILE__ . ':' . __LINE__);
        
        $primary = $table->getIdentifier();
        $elementOrder = 0;
        
        foreach ($tableCols as $name => $definition) {
            if (in_array($name, $this->_ignoreColumns)) {
                continue;
            }
            
            if ($element = $this->_generateElement($name, $definition)) {
                $this->addElement($element);
            }
        }
        
        if ($this->_withRelations) {
            foreach ($relations as $alias => $relation) {
                if (in_array($alias, $this->_ignoreRelations)) {
                    continue;
                }
                
                $rTable = $relation->getTable();
                $rIdentifier = $rTable->getIdentifier();
                
                if (is_array($rIdentifier)) {
                    continue;
                }
                
                if ($relation instanceof Doctrine_Relation_LocalKey) {
                    // One to Many
                    
                    $options = array('' => '------');
                    foreach ($rTable->findAll() as $row) {
                        $options[$row->$rIdentifier] = (string) $row;
                    }
                    
                    $rLocal = $relation->getLocal();
        
                    $definition = $rTable->getColumnDefinition($rLocal);
                    if (empty($definition['comment'])) {
                        $filter = self::getLabelFilter();
                        $label = ucfirst($filter->filter($rLocal));
                    } else {
                        $label = $definition['comment'];
                    }
                    
                    $this->addElement('select', $alias, array(
                        'label' => $label,
                        'multiOptions' => $options));
                } elseif ($relation instanceof Doctrine_Relation_Association ||
                          $relation instanceof Doctrine_Relation_Nest) {
                    // Many to Many OR Nested
                    $options = array();
                    
                    foreach ($rTable->findAll() as $row) {
                        $options[$row->$rIdentifier] = (string) $row;
                    }
                    
                    $rAssocTable = $relation->getAssociationTable();
                    $rForeign = $relation->getForeign();
                    $rForDef = $rAssocTable->getColumnDefinition($rForeign);
                    
                    if (empty($rForDef['comment'])) {
                        $filter = self::getLabelFilter();
                        $label = ucfirst($filter->filter($alias));
                        
                        if ($relation instanceof Doctrine_Relation_Nest) {
                            $label = 'Parent ' . $label;
                        }
                    } else {
                        $label = $rForDef['comment'];
                    }
                    
                    $this->addElement('multiCheckbox', $alias, array(
                        'label'        => $label,
                        'multiOptions' => $options
                    ));
                }
            }
        }
        
        $this->addElement('submit', '___save', array(
            'label' => 'Save',
            'order' => 1000));
        
        return $this;
    }
    
    /**
     * Generates form from the given $record
     *
     * @param  Doctrine_Record $record         record to generate and populate
     *                                         form from
     * @param  bool            $withRelations  [optional] defaults to true
     * @return Losc_Form_Doctrine
     */
    public function generateFromRecord(Doctrine_Record $record,
                                       $withRelations = null)
    {
        $this->generateFromTable($record->getTable(), $withRelations);
        $this->populateFromRecord($record);
        return $this;
    }
    
    /**
     * Populates form from the given $record
     *
     * @param  Doctrine_Record $record  record to populate form from
     * @return Losc_Form_Doctrine
     */
    public function populateFromRecord(Doctrine_Record $record)
    {
        $values = $record->toArray();
        
        foreach ($record->getTable()->getRelations() as $alias => $relation) {
            $identifier = $relation->getTable()->getIdentifier();
            
            if ($relation instanceof Doctrine_Relation_LocalKey) {
                if (is_string($record->$alias)) {
                    parent::populate(array($alias => $record->$alias));
                } elseif (is_object($record->$alias)) {
                    parent::populate(array($alias => $record->$alias->$identifier));
                }
            } elseif ($relation instanceof Doctrine_Relation_Association ||
                $relation instanceof Doctrine_Relation_Nest) {
                $options = array();
                foreach ($record->$alias as $option) {
                    $options[] = $option->$identifier;
                }
                parent::populate(array($alias => $options));
            }
            
            unset($values[$alias]);
        }
        
        return parent::populate($values);
    }
    
    /**
     * This is necessary until {@link Doctrine_Record::synchronizeWithArray()}
     * does link() and unlink()
     *
     * @param  Doctrine_Record $record  record to save
     * @return boolean
     */
    public function save(Doctrine_Record $record)
    {
        $relations = $record->getTable()->getRelations();
        
        foreach ($this->getElements() as $name => $element) {
            if (!in_array($name, $this->_ignoreColumns) &&
                !in_array($name, array_keys($relations))) {
                $record->$name = $element->getValue();
            }
        }
        
        try {
            Doctrine_Manager::connection()->beginTransaction();
            
            foreach ($relations as $alias => $relation) {
                if (!in_array($alias, $this->_ignoreRelations)) {
                    if ($relation instanceof Doctrine_Relation_LocalKey) {
                        $column = $relation->getLocal();
                        $record->$column = $this->getElement($alias)->getValue();
                        unset($relations[$alias]);
                    }
                }
            }
            
            $record->save();
            
            foreach ($relations as $alias => $relation) {
                if (!in_array($alias, $this->_ignoreRelations)) {
                    $record->unlink($alias);
                    $record->link($alias, $this->getElement($alias)->getValue());
                }
            }
            
            return Doctrine_Manager::connection()->commit();
            
        } catch (Exception $e) {
            Doctrine_Manager::connection()->rollback();
            throw $e;
        }
    }
    
    /**
     * Specifies that a relation should be ignored when generating form
     *
     * @param  string  $name    relation name
     * @param  boolean $ignore  [optional] whether the relation should be
     *                          ignored, default is true
     * @return Losc_Form_Doctrine
     */
    public function ignoreRelation($name, $ignore = true)
    {
        if (false === $ignore && in_array($name, $this->_ignoreRelations)) {
            foreach(array_keys($this->_ignoreRelations, $name) as $key) {
                unset($this->_ignoreRelations[$key]);
            }
        } elseif ($ignore && !in_array($name, $this->_ignoreRelations)) {
            $this->_ignoreRelations[] = $name;
        }
        
        return $this;
    }
    
    /**
     * Specifies relations that should be ignored when generating form
     *
     * @param  array   $relations  relations to ignore
     * @param  boolean $ignore     [optional] whether the relation should be
     *                             ignored, default is true
     * @return Losc_Form_Doctrine
     */
    public function ignoreRelations($relations = array(), $ignore = true)
    {
        if (empty($relations)) {
            if ($ignore) {
                $this->_withRelations = false;
            } else {
                $this->_ignoreRelations = array();
                $this->_withRelations = true;
            }
        } else {
            foreach ($relations as $relation) {
                $this->ignoreRelation($relation, $ignore);
            }
        }
        
        return $this;
    }
    
    /**
     * Specifies that a column should be ignored when generating form
     *
     * @param  string  $name    column name
     * @param  boolean $ignore  [optional] whether the relation should be
     *                          ignored, default is true
     * @return Losc_Form_Doctrine
     */
    public function ignoreColumn($name, $ignore = true)
    {
        if (!$ignore && in_array($name, $this->_ignoreColumns)) {
            foreach(array_keys($this->_ignoreColumns, $name) as $key) {
                unset($this->_ignoreColumns[$key]);
            }
        } elseif ($ignore && !in_array($name, $this->_ignoreColumns)) {
            $this->_ignoreColumns[] = $name;
        }
        
        return $this;
    }
    
    /**
     * Specifies columns that should be ignored when generating form
     *
     * @param  array   $columns
     * @param  boolean $ignore
     * @return Losc_Form_Doctrine
     */
    public function ignoreColumns($columns = array(), $ignore = true)
    {
        if (empty($columns)) {
            if (false === $ignore) {
                $this->_ignoreColumns = array();
            }
        } else {
            foreach ($columns as $column) {
                $this->ignoreColumn($column, $ignore);
            }
        }
    }
 
    /**
     * Generates element for a field
     *
     * @param  string $name        field name
     * @param  array  $definition  column defintion
     * @return Zend_Form_Element
     */
    protected function _generateElement($name, $definition)
    {
        if (empty($this->_columnElements[$name]) ||
            !$type = $this->_columnElements[$name]) {
            
            if (!isset($this->_fieldTypes[$definition['type']]) ||
                null === $this->_fieldTypes[$definition['type']]) {
                return null;
            }
            
            if ($definition['type'] == 'string') {
                $key = (empty($definition['length']) ||
                        $definition['length'] > 255) ? 1 : 0;
                $type = $this->_fieldTypes[$definition['type']][$key];
            } elseif (isset($definition['autoincrement']) &&
                      $definition['autoincrement'] === true) {
                $type = 'Hidden';
            } else {
                $type = $this->_fieldTypes[$definition['type']];
            }
        }
        
        if ($type == 'Hidden') {
            $label = null;
        } else if (empty($definition['comment'])) {
            $filter = self::getLabelFilter();
            $label = ucfirst($filter->filter($name));
        } else {
            $label = $definition['comment'];
        }
        
        $class = 'Zend_Form_Element_' . $type;
 
        $element = new $class($name, array(
            'id'       => null,
            'required' => empty($definition['notnull']) ?
                          false :
                          $definition['notnull'],
            'label'    => $label));
 
        if ($element instanceof Zend_Form_Element_Multi &&
            !empty($definition['values'])) {
            $element->addMultiOptions($definition['values']);
        }
        
        return $element;
    }

    /**
     * Override: Load the default decorators
     * 
     * @return void
     */
    public function loadDefaultDecorators()
    {
        if ($this->loadDefaultDecoratorsIsDisabled()) {
            return;
        }

        $decorators = $this->getDecorators();
        if (empty($decorators)) {
            //$this->addDecorator('Errors');
            $this->addDecorator('FormElements');
            $this->addDecorator('HtmlTag', array(
                'tag' => 'dl', 'class' => 'zend_form record'
            ));
            /*
            $this->addDecorator('Description', array(
                'escape'    => true,
                'tag'       => 'h4',
                'placement' => Zend_Form_Decorator_Abstract::PREPEND
            ));
            */
            $this->addDecorator('Form');
        }
    }

    /**
     * Renders form
     * 
     * @link http://framework.zend.com/issues/browse/ZF-2989
     * 
     * @param  Zend_View_Interface $view 
     * @return string
     */
    public function render(Zend_View_Interface $view = null)
    {
        $content = parent::render($view);
        return str_replace('<dt>&nbsp;</dt>', '<dt></dt>', $content);
    }
    
    /**
     * Returns filter to use for generating element labels
     *
     * @return Zend_Filter_Word_UnderScoreToSeparator
     */
    public static function getLabelFilter()
    {
        if (self::$_labelFilter == null) {
            self::$_labelFilter = new Zend_Filter_Word_UnderScoreToSeparator();
        }
        
        return self::$_labelFilter;
    }
}
